<audio title="39 _ 自增主键为什么不是连续的？" src="https://static001.geekbang.org/resource/audio/ff/c0/ffeae7cf89facbef6b0414c29726e1c0.mp3" controls="controls"></audio> 
<p>在<a href="https://time.geekbang.org/column/article/69236">第4篇文章</a>中，我们提到过自增主键，由于自增主键可以让主键索引尽量地保持递增顺序插入，避免了页分裂，因此索引更紧凑。</p><p>之前我见过有的业务设计依赖于自增主键的连续性，也就是说，这个设计假设自增主键是连续的。但实际上，这样的假设是错的，因为自增主键不能保证连续递增。</p><p>今天这篇文章，我们就来说说这个问题，看看什么情况下自增主键会出现 “空洞”？</p><p>为了便于说明，我们创建一个表t，其中id是自增主键字段、c是唯一索引。</p><pre><code>CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `c` int(11) DEFAULT NULL,
  `d` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB;
</code></pre><h1>自增值保存在哪儿？</h1><p>在这个空表t里面执行insert into t values(null, 1, 1);插入一行数据，再执行show create table命令，就可以看到如下图所示的结果：</p><p><img src="https://static001.geekbang.org/resource/image/cb/ff/cb2637cada0201b18650f56875e94fff.png" alt=""></p><center><span class="reference">图1 自动生成的AUTO_INCREMENT值</span></center><p>可以看到，表定义里面出现了一个AUTO_INCREMENT=2，表示下一次插入数据时，如果需要自动生成自增值，会生成id=2。</p><p>其实，这个输出结果容易引起这样的误解：自增值是保存在表结构定义里的。实际上，<strong>表的结构定义存放在后缀名为.frm的文件中，但是并不会保存自增值。</strong></p><p>不同的引擎对于自增值的保存策略不同。</p><ul>
<li>MyISAM引擎的自增值保存在数据文件中。</li>
<li>InnoDB引擎的自增值，其实是保存在了内存里，并且到了MySQL 8.0版本后，才有了“自增值持久化”的能力，也就是才实现了“如果发生重启，表的自增值可以恢复为MySQL重启前的值”，具体情况是：
<ul>
<li>在MySQL 5.7及之前的版本，自增值保存在内存里，并没有持久化。每次重启后，第一次打开表的时候，都会去找自增值的最大值max(id)，然后将max(id)+1作为这个表当前的自增值。﻿<br>
举例来说，如果一个表当前数据行里最大的id是10，AUTO_INCREMENT=11。这时候，我们删除id=10的行，AUTO_INCREMENT还是11。但如果马上重启实例，重启后这个表的AUTO_INCREMENT就会变成10。﻿<br>
也就是说，MySQL重启可能会修改一个表的AUTO_INCREMENT的值。</li>
<li>在MySQL 8.0版本，将自增值的变更记录在了redo log中，重启的时候依靠redo log恢复重启之前的值。</li>
</ul>
</li>
</ul><!-- [[[read_end]]] --><p>理解了MySQL对自增值的保存策略以后，我们再看看自增值修改机制。</p><h1>自增值修改机制</h1><p>在MySQL里面，如果字段id被定义为AUTO_INCREMENT，在插入一行数据的时候，自增值的行为如下：</p><ol>
<li>
<p>如果插入数据时id字段指定为0、null 或未指定值，那么就把这个表当前的 AUTO_INCREMENT值填到自增字段；</p>
</li>
<li>
<p>如果插入数据时id字段指定了具体的值，就直接使用语句里指定的值。</p>
</li>
</ol><p>根据要插入的值和当前自增值的大小关系，自增值的变更结果也会有所不同。假设，某次要插入的值是X，当前的自增值是Y。</p><ol>
<li>
<p>如果X&lt;Y，那么这个表的自增值不变；</p>
</li>
<li>
<p>如果X≥Y，就需要把当前自增值修改为新的自增值。</p>
</li>
</ol><p><strong>新的自增值生成算法是</strong>：从auto_increment_offset开始，以auto_increment_increment为步长，持续叠加，直到找到第一个大于X的值，作为新的自增值。</p><p>其中，auto_increment_offset 和 auto_increment_increment是两个系统参数，分别用来表示自增的初始值和步长，默认值都是1。</p><blockquote>
<p>备注：在一些场景下，使用的就不全是默认值。比如，双M的主备结构里要求双写的时候，我们就可能会设置成auto_increment_increment=2，让一个库的自增id都是奇数，另一个库的自增id都是偶数，避免两个库生成的主键发生冲突。</p>
</blockquote><p>当auto_increment_offset和auto_increment_increment都是1的时候，新的自增值生成逻辑很简单，就是：</p><ol>
<li>
<p>如果准备插入的值&gt;=当前自增值，新的自增值就是“准备插入的值+1”；</p>
</li>
<li>
<p>否则，自增值不变。</p>
</li>
</ol><p>这就引入了我们文章开头提到的问题，在这两个参数都设置为1的时候，自增主键id却不能保证是连续的，这是什么原因呢？</p><h1>自增值的修改时机</h1><p>要回答这个问题，我们就要看一下自增值的修改时机。</p><p>假设，表t里面已经有了(1,1,1)这条记录，这时我再执行一条插入数据命令：</p><pre><code>insert into t values(null, 1, 1); 
</code></pre><p>这个语句的执行流程就是：</p><ol>
<li>
<p>执行器调用InnoDB引擎接口写入一行，传入的这一行的值是(0,1,1);</p>
</li>
<li>
<p>InnoDB发现用户没有指定自增id的值，获取表t当前的自增值2；</p>
</li>
<li>
<p>将传入的行的值改成(2,1,1);</p>
</li>
<li>
<p>将表的自增值改成3；</p>
</li>
<li>
<p>继续执行插入数据操作，由于已经存在c=1的记录，所以报Duplicate key error，语句返回。</p>
</li>
</ol><p>对应的执行流程图如下：</p><p><img src="https://static001.geekbang.org/resource/image/f1/d3/f16d89a6e7ad6e2cde13b32bb2292dd3.jpg" alt=""></p><center><span class="reference">图2 insert(null, 1,1)唯一键冲突</span></center><p>可以看到，这个表的自增值改成3，是在真正执行插入数据的操作之前。这个语句真正执行的时候，因为碰到唯一键c冲突，所以id=2这一行并没有插入成功，但也没有将自增值再改回去。</p><p>所以，在这之后，再插入新的数据行时，拿到的自增id就是3。也就是说，出现了自增主键不连续的情况。</p><p>如图3所示就是完整的演示结果。</p><p><img src="https://static001.geekbang.org/resource/image/77/26/77b87820b649692a555f19b562d5d926.png" alt=""></p><center><span class="reference">图3 一个自增主键id不连续的复现步骤</span></center><p>可以看到，这个操作序列复现了一个自增主键id不连续的现场(没有id=2的行）。可见，<strong>唯一键冲突是导致自增主键id不连续的第一种原因。</strong></p><p>同样地，事务<strong>回滚也会产生类似的现象，这就是第二种原因。</strong></p><p>下面这个语句序列就可以构造不连续的自增id，你可以自己验证一下。</p><pre><code>insert into t values(null,1,1);
begin;
insert into t values(null,2,2);
rollback;
insert into t values(null,2,2);
//插入的行是(3,2,2)
</code></pre><p>你可能会问，为什么在出现唯一键冲突或者回滚的时候，MySQL没有把表t的自增值改回去呢？如果把表t的当前自增值从3改回2，再插入新数据的时候，不就可以生成id=2的一行数据了吗？</p><p>其实，MySQL这么设计是为了提升性能。接下来，我就跟你分析一下这个设计思路，看看<strong>自增值为什么不能回退。</strong></p><p>假设有两个并行执行的事务，在申请自增值的时候，为了避免两个事务申请到相同的自增id，肯定要加锁，然后顺序申请。</p><ol>
<li>
<p>假设事务A申请到了id=2， 事务B申请到id=3，那么这时候表t的自增值是4，之后继续执行。</p>
</li>
<li>
<p>事务B正确提交了，但事务A出现了唯一键冲突。</p>
</li>
<li>
<p>如果允许事务A把自增id回退，也就是把表t的当前自增值改回2，那么就会出现这样的情况：表里面已经有id=3的行，而当前的自增id值是2。</p>
</li>
<li>
<p>接下来，继续执行的其他事务就会申请到id=2，然后再申请到id=3。这时，就会出现插入语句报错“主键冲突”。</p>
</li>
</ol><p>而为了解决这个主键冲突，有两种方法：</p><ol>
<li>
<p>每次申请id之前，先判断表里面是否已经存在这个id。如果存在，就跳过这个id。但是，这个方法的成本很高。因为，本来申请id是一个很快的操作，现在还要再去主键索引树上判断id是否存在。</p>
</li>
<li>
<p>把自增id的锁范围扩大，必须等到一个事务执行完成并提交，下一个事务才能再申请自增id。这个方法的问题，就是锁的粒度太大，系统并发能力大大下降。</p>
</li>
</ol><p>可见，这两个方法都会导致性能问题。造成这些麻烦的罪魁祸首，就是我们假设的这个“允许自增id回退”的前提导致的。</p><p>因此，InnoDB放弃了这个设计，语句执行失败也不回退自增id。也正是因为这样，所以才只保证了自增id是递增的，但不保证是连续的。</p><h1>自增锁的优化</h1><p>可以看到，自增id锁并不是一个事务锁，而是每次申请完就马上释放，以便允许别的事务再申请。其实，在MySQL 5.1版本之前，并不是这样的。</p><p>接下来，我会先给你介绍下自增锁设计的历史，这样有助于你分析接下来的一个问题。</p><p>在MySQL 5.0版本的时候，自增锁的范围是语句级别。也就是说，如果一个语句申请了一个表自增锁，这个锁会等语句执行结束以后才释放。显然，这样设计会影响并发度。</p><p>MySQL 5.1.22版本引入了一个新策略，新增参数innodb_autoinc_lock_mode，默认值是1。</p><ol>
<li>
<p>这个参数的值被设置为0时，表示采用之前MySQL 5.0版本的策略，即语句执行结束后才释放锁；</p>
</li>
<li>
<p>这个参数的值被设置为1时：</p>
<ul>
<li>普通insert语句，自增锁在申请之后就马上释放；</li>
<li>类似insert … select这样的批量插入数据的语句，自增锁还是要等语句结束后才被释放；</li>
</ul>
</li>
<li>
<p>这个参数的值被设置为2时，所有的申请自增主键的动作都是申请后就释放锁。</p>
</li>
</ol><p>你一定有两个疑问：<strong>为什么默认设置下，insert … select 要使用语句级的锁？为什么这个参数的默认值不是2？</strong></p><p>答案是，这么设计还是为了数据的一致性。</p><p>我们一起来看一下这个场景：</p><p><img src="https://static001.geekbang.org/resource/image/e0/df/e0a69e151277de54a8262657e4ec89df.png" alt=""></p><center><span class="reference">图4 批量插入数据的自增锁</span></center><p>在这个例子里，我往表t1中插入了4行数据，然后创建了一个相同结构的表t2，然后两个session同时执行向表t2中插入数据的操作。</p><p>你可以设想一下，如果session B是申请了自增值以后马上就释放自增锁，那么就可能出现这样的情况：</p><ul>
<li>session B先插入了两个记录，(1,1,1)、(2,2,2)；</li>
<li>然后，session A来申请自增id得到id=3，插入了（3,5,5)；</li>
<li>之后，session B继续执行，插入两条记录(4,3,3)、 (5,4,4)。</li>
</ul><p>你可能会说，这也没关系吧，毕竟session B的语义本身就没有要求表t2的所有行的数据都跟session A相同。</p><p>是的，从数据逻辑上看是对的。但是，如果我们现在的binlog_format=statement，你可以设想下，binlog会怎么记录呢？</p><p>由于两个session是同时执行插入数据命令的，所以binlog里面对表t2的更新日志只有两种情况：要么先记session A的，要么先记session B的。</p><p>但不论是哪一种，这个binlog拿去从库执行，或者用来恢复临时实例，备库和临时实例里面，session B这个语句执行出来，生成的结果里面，id都是连续的。这时，这个库就发生了数据不一致。</p><p>你可以分析一下，出现这个问题的原因是什么？</p><p>其实，这是因为原库session B的insert语句，生成的id不连续。这个不连续的id，用statement格式的binlog来串行执行，是执行不出来的。</p><p>而要解决这个问题，有两种思路：</p><ol>
<li>
<p>一种思路是，让原库的批量插入数据语句，固定生成连续的id值。所以，自增锁直到语句执行结束才释放，就是为了达到这个目的。</p>
</li>
<li>
<p>另一种思路是，在binlog里面把插入数据的操作都如实记录进来，到备库执行的时候，不再依赖于自增主键去生成。这种情况，其实就是innodb_autoinc_lock_mode设置为2，同时binlog_format设置为row。</p>
</li>
</ol><p>因此，<strong>在生产上，尤其是有insert … select这种批量插入数据的场景时，从并发插入数据性能的角度考虑，我建议你这样设置：innodb_autoinc_lock_mode=2 ，并且 binlog_format=row</strong>.这样做，既能提升并发性，又不会出现数据一致性问题。</p><p>需要注意的是，我这里说的<strong>批量插入数据，包含的语句类型是insert … select、replace … select和load data语句。</strong></p><p>但是，在普通的insert语句里面包含多个value值的情况下，即使innodb_autoinc_lock_mode设置为1，也不会等语句执行完成才释放锁。因为这类语句在申请自增id的时候，是可以精确计算出需要多少个id的，然后一次性申请，申请完成后锁就可以释放了。</p><p>也就是说，批量插入数据的语句，之所以需要这么设置，是因为“不知道要预先申请多少个id”。</p><p>既然预先不知道要申请多少个自增id，那么一种直接的想法就是需要一个时申请一个。但如果一个select … insert语句要插入10万行数据，按照这个逻辑的话就要申请10万次。显然，这种申请自增id的策略，在大批量插入数据的情况下，不但速度慢，还会影响并发插入的性能。</p><p>因此，对于批量插入数据的语句，MySQL有一个批量申请自增id的策略：</p><ol>
<li>
<p>语句执行过程中，第一次申请自增id，会分配1个；</p>
</li>
<li>
<p>1个用完以后，这个语句第二次申请自增id，会分配2个；</p>
</li>
<li>
<p>2个用完以后，还是这个语句，第三次申请自增id，会分配4个；</p>
</li>
<li>
<p>依此类推，同一个语句去申请自增id，每次申请到的自增id个数都是上一次的两倍。</p>
</li>
</ol><p>举个例子，我们一起看看下面的这个语句序列：</p><pre><code>insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);
create table t2 like t;
insert into t2(c,d) select c,d from t;
insert into t2 values(null, 5,5);
</code></pre><p>insert…select，实际上往表t2中插入了4行数据。但是，这四行数据是分三次申请的自增id，第一次申请到了id=1，第二次被分配了id=2和id=3， 第三次被分配到id=4到id=7。</p><p>由于这条语句实际只用上了4个id，所以id=5到id=7就被浪费掉了。之后，再执行insert into t2 values(null, 5,5)，实际上插入的数据就是（8,5,5)。</p><p><strong>这是主键id出现自增id不连续的第三种原因。</strong></p><h1>小结</h1><p>今天，我们从“自增主键为什么会出现不连续的值”这个问题开始，首先讨论了自增值的存储。</p><p>在MyISAM引擎里面，自增值是被写在数据文件上的。而在InnoDB中，自增值是被记录在内存的。MySQL直到8.0版本，才给InnoDB表的自增值加上了持久化的能力，确保重启前后一个表的自增值不变。</p><p>然后，我和你分享了在一个语句执行过程中，自增值改变的时机，分析了为什么MySQL在事务回滚的时候不能回收自增id。</p><p>MySQL 5.1.22版本开始引入的参数innodb_autoinc_lock_mode，控制了自增值申请时的锁范围。从并发性能的角度考虑，我建议你将其设置为2，同时将binlog_format设置为row。我在前面的文章中其实多次提到，binlog_format设置为row，是很有必要的。今天的例子给这个结论多了一个理由。</p><p>最后，我给你留一个思考题吧。</p><p>在最后一个例子中，执行insert into t2(c,d) select c,d from t;这个语句的时候，如果隔离级别是可重复读（repeatable read），binlog_format=statement。这个语句会对表t的所有记录和间隙加锁。</p><p>你觉得为什么需要这么做呢？</p><p>你可以把你的思考和分析写在评论区，我会在下一篇文章和你讨论这个问题。感谢你的收听，也欢迎你把这篇文章分享给更多的朋友一起阅读。</p><h1>上期问题时间</h1><p>上期的问题是，如果你维护的MySQL系统里有内存表，怎么避免内存表突然丢数据，然后导致主备同步停止的情况。</p><p>我们假设的是主库暂时不能修改引擎，那么就把备库的内存表引擎先都改成InnoDB。对于每个内存表，执行</p><pre><code>set sql_log_bin=off;
alter table tbl_name engine=innodb;
</code></pre><p>这样就能避免备库重启的时候，数据丢失的问题。</p><p>由于主库重启后，会往binlog里面写“delete from tbl_name”，这个命令传到备库，备库的同名的表数据也会被清空。</p><p>因此，就不会出现主备同步停止的问题。</p><p>如果由于主库异常重启，触发了HA，这时候我们之前修改过引擎的备库变成了主库。而原来的主库变成了新备库，在新备库上把所有的内存表（这时候表里没数据）都改成InnoDB表。</p><p>所以，如果我们不能直接修改主库上的表引擎，可以配置一个自动巡检的工具，在备库上发现内存表就把引擎改了。</p><p>同时，跟业务开发同学约定好建表规则，避免创建新的内存表。</p><p>评论区留言点赞板：</p><blockquote>
<p>大家在春节期间还坚持看专栏，并且深入地思考和回复，给大家点赞。<br>
@长杰 同学提到的将数据保存到InnoDB表用来持久化，也是一个方法。不过，我还是建议釜底抽薪，直接修改备库的内存表的引擎。<br>
@老杨同志 提到的是主库异常重启的场景，这时候是不会报主备不一致的，因为主库重启的时候写了delete from tbl_name，主备的内存表都清空了。</p>
</blockquote><p></p>
<style>
    ul {
      list-style: none;
      display: block;
      list-style-type: disc;
      margin-block-start: 1em;
      margin-block-end: 1em;
      margin-inline-start: 0px;
      margin-inline-end: 0px;
      padding-inline-start: 40px;
    }
    li {
      display: list-item;
      text-align: -webkit-match-parent;
    }
    ._2sjJGcOH_0 {
      list-style-position: inside;
      width: 100%;
      display: -webkit-box;
      display: -ms-flexbox;
      display: flex;
      -webkit-box-orient: horizontal;
      -webkit-box-direction: normal;
      -ms-flex-direction: row;
      flex-direction: row;
      margin-top: 26px;
      border-bottom: 1px solid rgba(233,233,233,0.6);
    }
    ._2sjJGcOH_0 ._3FLYR4bF_0 {
      width: 34px;
      height: 34px;
      -ms-flex-negative: 0;
      flex-shrink: 0;
      border-radius: 50%;
    }
    ._2sjJGcOH_0 ._36ChpWj4_0 {
      margin-left: 0.5rem;
      -webkit-box-flex: 1;
      -ms-flex-positive: 1;
      flex-grow: 1;
      padding-bottom: 20px;
    }
    ._2sjJGcOH_0 ._36ChpWj4_0 ._2zFoi7sd_0 {
      font-size: 16px;
      color: #3d464d;
      font-weight: 500;
      -webkit-font-smoothing: antialiased;
      line-height: 34px;
    }
    ._2sjJGcOH_0 ._36ChpWj4_0 ._2_QraFYR_0 {
      margin-top: 12px;
      color: #505050;
      -webkit-font-smoothing: antialiased;
      font-size: 14px;
      font-weight: 400;
      white-space: normal;
      word-break: break-all;
      line-height: 24px;
    }
    ._2sjJGcOH_0 ._10o3OAxT_0 {
      margin-top: 18px;
      border-radius: 4px;
      background-color: #f6f7fb;
    }
    ._2sjJGcOH_0 ._3klNVc4Z_0 {
      display: -webkit-box;
      display: -ms-flexbox;
      display: flex;
      -webkit-box-orient: horizontal;
      -webkit-box-direction: normal;
      -ms-flex-direction: row;
      flex-direction: row;
      -webkit-box-pack: justify;
      -ms-flex-pack: justify;
      justify-content: space-between;
      -webkit-box-align: center;
      -ms-flex-align: center;
      align-items: center;
      margin-top: 15px;
    }
    ._2sjJGcOH_0 ._10o3OAxT_0 ._3KxQPN3V_0 {
      color: #505050;
      -webkit-font-smoothing: antialiased;
      font-size: 14px;
      font-weight: 400;
      white-space: normal;
      word-break: break-word;
      padding: 20px 20px 20px 24px;
    }
    ._2sjJGcOH_0 ._3klNVc4Z_0 {
      display: -webkit-box;
      display: -ms-flexbox;
      display: flex;
      -webkit-box-orient: horizontal;
      -webkit-box-direction: normal;
      -ms-flex-direction: row;
      flex-direction: row;
      -webkit-box-pack: justify;
      -ms-flex-pack: justify;
      justify-content: space-between;
      -webkit-box-align: center;
      -ms-flex-align: center;
      align-items: center;
      margin-top: 15px;
    }
    ._2sjJGcOH_0 ._3Hkula0k_0 {
      color: #b2b2b2;
      font-size: 14px;
    }
</style><ul><li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/14/05/d4/e06bf86d.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>长杰</span>
  </div>
  <div class="_2_QraFYR_0">在最后一个例子中，执行 insert into t2(c,d) select c,d from t; 这个语句的时候，如果隔离级别是可重复读（repeatable read），binlog_format=statement。这个语句会对表 t 的所有记录和间隙加锁。<br>你觉得为什么需要这么做呢？<br>假如原库不对t表所有记录和间隙加锁，如果有其他事物新增数据并先与这个批量操作提交，由于事物的隔离级别是可重复读，t2是看不到新增的数据的。但是记录的binlog是statement格式，备库或基于binlog恢复的临时库，t2会看到新增的数据，出现数据不一致的情况。</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 👍 这是一个典型的场景</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-11 15:57:57</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/12/e8/ea/a4568138.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>Nomius</span>
  </div>
  <div class="_2_QraFYR_0">不知道老师还关不关注. <br>(1)问一下为什么一张表上面只能有一个自增的字段?  (这个大概能从文章中分析出来,因为autoincrement是定义在表结构中,如果有多个的话实现自增的时候逻辑太复杂了)<br>(2)为什么自增的字段上面必须要有索引?  </div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 1. 是的<br>2. 我觉得最初的一个原因是，由于以前（8.0版本前）自增主键值是不持久化的，只放在内存里面。每次重启后，重新打开表时，需要计算“自增字段里面的最大值”，然后加1，作为当前的autoincrement的值。<br>如果没有索引，算这个值就要做全表扫描，性能可能很差，影响访问表的速度。<br>好问题。不过这个只是我个人猜测，也可能还有别的原因。😆</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-06-05 14:06:54</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/0f/67/f4/9a1feb59.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>钱</span>
  </div>
  <div class="_2_QraFYR_0">最喜欢这样的文章，以为比较简单和熟悉，也能打开一扇窗，让人看到一个不同的世界，并且无比丰富多彩。<br>在什么场景下自增主键可能不连续？<br>1：唯一键冲突<br>2：事务回滚<br>3：自增主键的批量申请<br>深层次原因是，不判断自增主键是否已存在和减少加锁的时间范围和粒度-&gt;为了更高的性能-&gt;自增主键不能回退-&gt;自增主键不连续<br>自增主键是怎么做的唯一性的？<br>自增值加1，自增锁控制并发<br>自增主键的生成性能如何？<br>这个需要测试一下，数据库的自增主键也用做生成唯一数字，作为其他单号，比如：并发量小的订单号，性能可能一般。<br>自增主键有最大值嘛？如果有，到了咋弄？<br>最大值应该有，因为数字总有个范围，到了当做字符串的一部分，然后再自增拼接上另一部分，貌似也可以。<br>自增主键的作用？保存机制？修改机制？<br>作用：让主键索引尽量地保持递增顺序插入，避免页分裂，使索引更紧凑。<br>保存机制：不同的存储引擎不一样。<br>MyISAM 引擎的自增值保存在数据文件中。<br>InnoDB 引擎的自增值，先是保存在了内存里，到了 MySQL 8.0 版本后，才有了“自增值持久化”的能力，放在了redolog里。<br>修改机制：<br>在 MySQL 里面，如果字段 id 被定义为 AUTO_INCREMENT，在插入一行数据的时候，自增值的行为如下：<br>1：如果插入数据时 id 字段指定为 0、null 或未指定值，那么就把这个表当前的 AUTO_INCREMENT 值填到自增字段；<br>2：如果插入数据时 id 字段指定了具体的值，就直接使用语句里指定的值。<br><br>根据要插入的值和当前自增值的大小关系，自增值的变更结果也会有所不同。假设，某次要插入的值是 X，当前的自增值是 Y。<br>1：如果 X&lt;Y，那么这个表的自增值不变；<br>2：如果 X≥Y，就需要把当前自增值修改为新的自增值。</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 👍</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-08-07 07:32:07</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/11/ec/fd/bad2abf5.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>帽子掉了</span>
  </div>
  <div class="_2_QraFYR_0">老师您好，我有一个时序问题，想请教一下。<br>从这篇文章的介绍来看，获取自增id和写binlog是有先后顺序的。<br>那么在binlog为statement的情况下。<br>语句A先获取id=1，然后B获取id=2，接着B提交，写binlog，再A写binlog。<br>这个时候如果binlog重放，是不是会发生B的id为1，而A的id为2的不一致的情况？</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 好问题，不会<br>因为binlog在记录这种带自增值的语句之前，会在前面多一句，用于指定“接下来这个语句要需要的 自增ID值是多少”，而这个值，是在主库上这一行插入成功后对应的自增值，所以是一致的</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-13 20:49:18</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/11/8a/3e/30c05bce.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>aliang</span>
  </div>
  <div class="_2_QraFYR_0">老师，我们这边有的开发不喜欢用mysql自带的主键自增功能，而是在程序中控制主键（时间+业务+机器+序列，bigint类型，实际长度有17位，其中序列保存在内存中，每次递增，主键值不连续）。理由是<br>（1）通过这样的主键可以直接定位数据，减少索引（2）如果自增，必须先存数据得到主键才可继续下面的程序，如果自己计算主键，可以在入库前进行异步处理<br>（3）a表要insert得到主键，然后处理b表，然后根据条件还要update a表。如果程序自己控制，就不用先insert a表，数据可以在内存中，直到最后一次提交。（对于a表，本来是insert+update，最后只是一条insert，少一次数据库操作）<br>我想请问的是：<br>（1）针对理由1，是否可以用组合索引替代？<br>（2）针对理由2，是否mysql自身的主键自增分配逻辑就已经能实现了？<br>（3）针对理由3，主键更长意味着更大的索引（主键索引和普通索引），你觉得怎样做会更好呢</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: “（时间+业务+机器+序列，bigint类型，实际长度有17位，其中序列保存在内存中，每次递增，主键值不连续）。” ----bigint就是8位，这个你需要确定一下。如果是8位的还好，如果是17位的字符串，就比较耗费空间；<br><br>（1）如果“序列”是递增的，还是不能直接用来体现业务逻辑吧？ 创建有业务意义的字段索引估计还是省不了的 ？<br>（2）mysql确实做不到“插入之前就先算好接下来的id是多少”，一般都是insert执行完成后，再执行select last_insert_id<br> (3) 先insert a再update b再update a，确实看上去比较奇怪，不过感觉这个逻辑应该是可以优化的，不应该作为“主键选择”的一个依据。你可否脱敏一下，把模拟的表结构和业务逻辑说下，看看是不是可以优化的。<br><br>总之，按照你说的“时间+业务+机器+序列”这种模式，有点像用uuid，主要的问题还是，如果这个表的索引多，占用的空间比较大</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-12 16:34:40</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/11/40/5e/b8fada94.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>Ryoma</span>
  </div>
  <div class="_2_QraFYR_0">在8.0.3版本后，innodb_autoinc_lock_mode默认值已是2，在binlog_format默认值为row的前提下，想来也是为了增加并发。 <br><br>https:&#47;&#47;dev.mysql.com&#47;doc&#47;refman&#47;8.0&#47;en&#47;innodb-parameters.html#sysvar_innodb_autoinc_lock_mode</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 👍 大势所趋😆</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-14 22:21:23</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/0f/87/dd/4f53f95d.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>进阶的码农</span>
  </div>
  <div class="_2_QraFYR_0">课后题<br>在最后一个例子中，执行 insert into t2(c,d) select c,d from t; 这个语句的时候，如果隔离级别是可重复读（repeatable read），binlog_format=statement会加记录锁和间隙锁。啥我的binlog_format=row也加锁了</div>
  <div class="_10o3OAxT_0">
    
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-03-12 10:43:39</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/12/03/c9/9a9d82ab.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>Aaron_涛</span>
  </div>
  <div class="_2_QraFYR_0">老师，能如果两个事务同时并发插入，主键没有指明的话，加锁的情况能说明下吗</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 是说自增主键没指定？<br><br>两个语句分别去申请自增主键，申请到的值是不一样的，所以并不冲突</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-04-21 22:31:33</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/10/0c/0f/93d1c8eb.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>mickey</span>
  </div>
  <div class="_2_QraFYR_0">在讲 insert … select 要使用语句级的锁 的例子时，Session A和B 是不是讲反了？？</div>
  <div class="_10o3OAxT_0">
    
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2020-03-16 10:34:53</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/0f/76/f5/e3f5bd8d.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>宝仔</span>
  </div>
  <div class="_2_QraFYR_0">老师这种情况<br>insert into t (id,c,d) values (1,1,1),(2,2,2),(3,3,3),(null,4,4);<br>或者<br>insert into t (id,c,d) values (1,1,1),(null,2,2),(3,3,3),(null,4,4);<br>能解释下吗？<br>第一种情况自增id变成8了<br>第二种情况自增id变成6了</div>
  <div class="_10o3OAxT_0">
    
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-12-10 20:56:59</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/0f/87/dd/4f53f95d.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>进阶的码农</span>
  </div>
  <div class="_2_QraFYR_0">上期问题解答，有点疑问<br>set sql_log_bin=off;<br>alter table tbl_name engine=innodb;<br><br>为什么备库需要执行set sql_log_bin=off这一句<br>把表的引擎改成innodb不就能解决重启后内存表被删除的问题吗？<br></div>
  <div class="_10o3OAxT_0">
    
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-03-12 11:33:17</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/10/1d/7f/aabc1b66.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>hetiu</span>
  </div>
  <div class="_2_QraFYR_0">老师，请问下innodb_autoinc_lock_mode配置是库级别的还是实例级别的？</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 全局的</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-03-05 10:18:43</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/0f/72/89/1a83120a.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>yihang</span>
  </div>
  <div class="_2_QraFYR_0">主键 id 出现自增 id 不连续的第三种原因 没有实验出来啊innodb_autoinc_lock_mode=0，1，2 都试了，mysql 版本是8.0, 最后自增列还是连续的</div>
  <div class="_10o3OAxT_0">
    
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2020-02-09 08:53:38</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="http://thirdwx.qlogo.cn/mmopen/vi_32/JKKWS6TzhncvAA0p0NDiaATPIvMicSM76vNAg9IG1ibibcJYPAiaicYjZfq4gAV8GRtcTpOibfRD8vzqHBtL0ibmhwQsbg/132"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>唐名之</span>
  </div>
  <div class="_2_QraFYR_0">老师，如果我业务场景必须需要一个带有序自增值，设业务为表A，另外添加一张表记录自增为表B，表B包含3个字段（自增主键，表A唯一键，自增列）；伪代码如下；这样能实现吗？或者有其他什么好的方案？<br>begin；<br>insert into A values（字段1, 唯一键）；<br>insert into B value (表A唯一键，自增列)；<br>commit；</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 这样思路上是ok的，<br><br>不过表b怎么有两个自增列？一个表只能有一个自增列。<br></p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-25 16:27:37</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/13/e2/c7/d6a0927a.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>AstonPutting</span>
  </div>
  <div class="_2_QraFYR_0">老师，innodb_autoinc_lock_mode = 2，binlog_format = statement 不也会出现数据不一致的问题吗？不是很理解 binlog_format = statement 的情况下，1 与 2 的区别。</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: innodb_autoinc_lock_mode = 2的时候就要binlog_format = row才好</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-21 15:28:57</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/13/f1/84/7d21bd9e.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>Goal</span>
  </div>
  <div class="_2_QraFYR_0">赶上了进度，把春节期间的补回来了</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 👍</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-12 11:49:15</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/10/f7/b1/982ea185.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>Frank</span>
  </div>
  <div class="_2_QraFYR_0">insert into t values(null,1,1);<br>begin;<br>insert into t values(null,2,2);<br>rolllack;<br>insert into t values(null,2,2);<br>&#47;&#47; 插入的行是 (3,2,2)<br><br>老师 里面是 rollback 吧</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 是的，我手残了。。<br><br>多谢指出，发起勘误了哈<br></p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-12 10:09:16</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/11/5b/fc/5c24080b.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>牛在天上飞</span>
  </div>
  <div class="_2_QraFYR_0">老师，请问产生大量的event事件会对mysql服务器有什么影响？主要是哪几个方面的影响？</div>
  <div class="_10o3OAxT_0">
    <p class="_3KxQPN3V_0">作者回复: 也没啥，主要就是不好管理。。<br>毕竟event是写在MySQL里的，写程序的同学不一定会记得。<br>比较建议将这类逻辑写在应用程序里面</p>
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2019-02-12 09:00:08</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/25/5b/95/462890c6.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>叶涛</span>
  </div>
  <div class="_2_QraFYR_0">有没有这种情况呢：<br>1. t表的先新增一条数据, 然后执行 insert into t2(c,d) select c,d from t，t表正常加锁,<br>2. 但如果binlog记录的是 insert into t2...select... t 和 insert t...<br>3. 在从库执行的时候先执行了insert into t2 select....，此时t新增的数据的sql还没有执行，这样是不是也是主备数据不一致了</div>
  <div class="_10o3OAxT_0">
    
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2021-05-08 18:36:14</div>
  </div>
</div>
</div>
</li>
<li>
<div class="_2sjJGcOH_0"><img src="https://static001.geekbang.org/account/avatar/00/25/61/de/94ee070a.jpg"
  class="_3FLYR4bF_0">
<div class="_36ChpWj4_0">
  <div class="_2zFoi7sd_0"><span>特兰克斯</span>
  </div>
  <div class="_2_QraFYR_0">老师, mybatis  加了   useGeneratedKeys=&quot;true&quot;   keyProperty=&quot;id&quot;  保存的时候返回id, 但是有时候缺没返回, 但是数据库确实是保存了,是什么情况呀</div>
  <div class="_10o3OAxT_0">
    
  </div>
  <div class="_3klNVc4Z_0">
    <div class="_3Hkula0k_0">2021-04-09 11:42:00</div>
  </div>
</div>
</div>
</li>
</ul>